{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Week 2 Day 2\n",
    "\n",
    "Our first Agentic Framework project!!\n",
    "\n",
    "Prepare yourself for something ridiculously easy.\n",
    "\n",
    "We're going to build a simple Agent system for generating cold sales outreach emails:\n",
    "1. Agent workflow\n",
    "2. Use of tools to call functions\n",
    "3. Agent collaboration via Tools and Handoffs"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Before we start - some setup:\n",
    "\n",
    "\n",
    "Please visit Pushover at: https://pushover.net/\n",
    "\n",
    "Pushover is a simple and reliable push notification service.\n",
    "\n",
    "Create a free account if you don't already have one.\n",
    "\n",
    "Once logged in:\n",
    "1. Create a new application at https://pushover.net/apps/build\n",
    "2. Note your application's \"API Token/Key\"\n",
    "3. Note your \"User Key\" from the main page\n",
    "\n",
    "Add these keys to your .env file:\n",
    "\n",
    "`PUSHOVER_TOKEN=xxxx`\n",
    "`PUSHOVER_USER=xxxx`\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dotenv import load_dotenv\n",
    "from agents import Agent, Runner, trace, function_tool\n",
    "from openai.types.responses import ResponseTextDeltaEvent\n",
    "from typing import Dict\n",
    "import requests\n",
    "import os\n",
    "import asyncio\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Let's just check notifications are working for you\n",
    "\n",
    "def send_test_notification():\n",
    "    \"\"\"Send a test notification via Pushover\"\"\"\n",
    "    url = \"https://api.pushover.net/1/messages.json\"\n",
    "    data = {\n",
    "        \"token\": os.environ.get('PUSHOVER_TOKEN'),\n",
    "        \"user\": os.environ.get('PUSHOVER_USER'),\n",
    "        \"message\": \"This is an important test notification\",\n",
    "        \"title\": \"Test notification\"\n",
    "    }\n",
    "    response = requests.post(url, data=data)\n",
    "    print(f\"Status: {response.status_code}\")\n",
    "    print(f\"Response: {response.json()}\")\n",
    "\n",
    "send_test_notification()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Did you receive the test notification?\n",
    "\n",
    "If you get status 200 and {\"status\": 1, \"request\": \"...\"}, then everything works perfectly!\n",
    "\n",
    "#### Possible errors\n",
    "\n",
    "If you have errors, check:\n",
    "1. That your PUSHOVER_TOKEN and PUSHOVER_USER keys are correctly defined in your .env\n",
    "2. That you have created an application on Pushover and copied the right token\n",
    "3. That your User Key is correct (visible on Pushover's main page)\n",
    "\n",
    "#### Dependencies installation\n",
    "\n",
    "If you don't have `requests` yet, install it:\n",
    "```\n",
    "!uv pip install requests\n",
    "```\n",
    "\n",
    "Pushover is much simpler and more reliable than email services for notifications!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 1: Agent workflow"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "instructions1 = \"You are a sales agent working for ComplAI, \\\n",
    "a company that provides a SaaS tool for ensuring SOC2 compliance and preparing for audits, powered by AI. \\\n",
    "You write professional, serious cold emails.\"\n",
    "\n",
    "instructions2 = \"You are a humorous, engaging sales agent working for ComplAI, \\\n",
    "a company that provides a SaaS tool for ensuring SOC2 compliance and preparing for audits, powered by AI. \\\n",
    "You write witty, engaging cold emails that are likely to get a response.\"\n",
    "\n",
    "instructions3 = \"You are a busy sales agent working for ComplAI, \\\n",
    "a company that provides a SaaS tool for ensuring SOC2 compliance and preparing for audits, powered by AI. \\\n",
    "You write concise, to the point cold emails.\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_agent1 = Agent(\n",
    "        name=\"Professional Sales Agent\",\n",
    "        instructions=instructions1,\n",
    "        model=\"gpt-4o-mini\"\n",
    ")\n",
    "\n",
    "sales_agent2 = Agent(\n",
    "        name=\"Engaging Sales Agent\",\n",
    "        instructions=instructions2,\n",
    "        model=\"gpt-4o-mini\"\n",
    ")\n",
    "\n",
    "sales_agent3 = Agent(\n",
    "        name=\"Busy Sales Agent\",\n",
    "        instructions=instructions3,\n",
    "        model=\"gpt-4o-mini\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "result = Runner.run_streamed(sales_agent1, input=\"Write a cold sales email\")\n",
    "async for event in result.stream_events():\n",
    "    if event.type == \"raw_response_event\" and isinstance(event.data, ResponseTextDeltaEvent):\n",
    "        print(event.data.delta, end=\"\", flush=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "message = \"Write a cold sales email\"\n",
    "\n",
    "with trace(\"Parallel cold emails\"):\n",
    "    results = await asyncio.gather(\n",
    "        Runner.run(sales_agent1, message),\n",
    "        Runner.run(sales_agent2, message),\n",
    "        Runner.run(sales_agent3, message),\n",
    "    )\n",
    "\n",
    "outputs = [result.final_output for result in results]\n",
    "\n",
    "for output in outputs:\n",
    "    print(output + \"\\n\\n\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_picker = Agent(\n",
    "    name=\"sales_picker\",\n",
    "    instructions=\"You pick the best cold sales email from the given options. \\\n",
    "Imagine you are a customer and pick the one you are most likely to respond to. \\\n",
    "Do not give an explanation; reply with the selected email only.\",\n",
    "    model=\"gpt-4o-mini\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "message = \"Write a cold sales email\"\n",
    "\n",
    "with trace(\"Selection from sales people\"):\n",
    "    results = await asyncio.gather(\n",
    "        Runner.run(sales_agent1, message),\n",
    "        Runner.run(sales_agent2, message),\n",
    "        Runner.run(sales_agent3, message),\n",
    "    )\n",
    "    outputs = [result.final_output for result in results]\n",
    "\n",
    "    emails = \"Cold sales emails:\\n\\n\" + \"\\n\\nEmail:\\n\\n\".join(outputs)\n",
    "\n",
    "    best = await Runner.run(sales_picker, emails)\n",
    "\n",
    "    print(f\"Best sales email:\\n{best.final_output}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now go and check out the trace:\n",
    "\n",
    "https://platform.openai.com/traces"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 2: use of tools\n",
    "\n",
    "Now we will add a tool to the mix.\n",
    "\n",
    "Remember all that json boilerplate and the `handle_tool_calls()` function with the if logic.."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_agent1 = Agent(\n",
    "        name=\"Professional Sales Agent\",\n",
    "        instructions=instructions1,\n",
    "        model=\"gpt-4o-mini\",\n",
    ")\n",
    "\n",
    "sales_agent2 = Agent(\n",
    "        name=\"Engaging Sales Agent\",\n",
    "        instructions=instructions2,\n",
    "        model=\"gpt-4o-mini\",\n",
    ")\n",
    "\n",
    "sales_agent3 = Agent(\n",
    "        name=\"Busy Sales Agent\",\n",
    "        instructions=instructions3,\n",
    "        model=\"gpt-4o-mini\",\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_agent1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Steps 2 and 3: Tools and Agent interactions\n",
    "\n",
    "Remember all that boilerplate json?\n",
    "\n",
    "Simply wrap your function with the decorator `@function_tool`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@function_tool\n",
    "def send_notification(body: str):\n",
    "    \"\"\" Send a notification with the given body to all sales prospects \"\"\"\n",
    "    url = \"https://api.pushover.net/1/messages.json\"\n",
    "    data = {\n",
    "        \"token\": os.environ.get('PUSHOVER_TOKEN'),\n",
    "        \"user\": os.environ.get('PUSHOVER_USER'),\n",
    "        \"message\": body,\n",
    "        \"title\": \"Sales Email\"\n",
    "    }\n",
    "    response = requests.post(url, data=data)\n",
    "    if response.status_code == 200:\n",
    "        return {\"status\": \"success\", \"message\": \"Notification sent successfully\"}\n",
    "    else:\n",
    "        return {\"status\": \"error\", \"message\": f\"Error {response.status_code}: {response.text}\"}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### This has automatically been converted into a tool, with the boilerplate json created"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Let's look at it\n",
    "send_notification"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### And you can also convert an Agent into a tool"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tool1 = sales_agent1.as_tool(tool_name=\"sales_agent1\", tool_description=\"Write a cold sales email\")\n",
    "tool1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### So now we can gather all the tools together:\n",
    "\n",
    "A tool for each of our 3 email-writing agents\n",
    "\n",
    "And a tool for our function to send emails"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description = \"Write a cold sales email\"\n",
    "\n",
    "tool1 = sales_agent1.as_tool(tool_name=\"sales_agent1\", tool_description=description)\n",
    "tool2 = sales_agent2.as_tool(tool_name=\"sales_agent2\", tool_description=description)\n",
    "tool3 = sales_agent3.as_tool(tool_name=\"sales_agent3\", tool_description=description)\n",
    "\n",
    "tools = [tool1, tool2, tool3, send_notification]\n",
    "\n",
    "tools"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## And now it's time for our Sales Manager - our planning agent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Improved instructions thanks to Jessica Kuijer\n",
    "\n",
    "instructions = \"\"\"\n",
    "You are a Sales Manager at ComplAI. Your goal is to find the single best cold sales email using the sales_agent tools.\n",
    " \n",
    "Follow these steps carefully:\n",
    "1. Generate Drafts: Use all three sales_agent tools to generate three different email drafts. Do not proceed until all three drafts are ready.\n",
    " \n",
    "2. Evaluate and Select: Review the drafts and choose the single best email using your judgment of which one is most effective.\n",
    " \n",
    "3. Use the send_notification tool to send the best email (and only the best email) to the user.\n",
    " \n",
    "Crucial Rules:\n",
    "- You must use the sales agent tools to generate the drafts — do not write them yourself.\n",
    "- You must send ONE notification using the send_notification tool — never more than one.\n",
    "\"\"\"\n",
    "\n",
    "\n",
    "sales_manager = Agent(name=\"Sales Manager\", instructions=instructions, tools=tools, model=\"gpt-4o-mini\")\n",
    "\n",
    "message = \"Send a cold sales email addressed to 'Dear CEO'\"\n",
    "\n",
    "with trace(\"Sales manager\"):\n",
    "    result = await Runner.run(sales_manager, message)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/stop.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#ff7800;\">Wait - you didn't get a notification??</h2>\n",
    "            <span style=\"color:#ff7800;\">If you don't receive a notification after running the previous cell, here are some things to check: <br/>\n",
    "            1. Check that your PUSHOVER_TOKEN and PUSHOVER_USER keys are correctly defined<br/>\n",
    "            2. Check your Pushover mobile app<br/>\n",
    "            3. Look at the trace in OpenAI to see if there are any errors<br/>\n",
    "            Pushover is much more reliable than emails - notifications usually arrive within seconds!\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Advantages of Pushover over emails:\n",
    "\n",
    "- **Instant**: notifications arrive within seconds\n",
    "- **Reliable**: no spam or SSL certificate issues\n",
    "- **Simple**: easier setup with just two API keys\n",
    "- **Multi-platform**: works on mobile, desktop and web\n",
    "- **No complex sending limits** like with email services\n",
    "\n",
    "Pushover is perfect for development notifications and alerts!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Remember to check the trace\n",
    "\n",
    "https://platform.openai.com/traces\n",
    "\n",
    "And then check your notification!!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Handoffs represent a way an agent can delegate to an agent, passing control to it\n",
    "\n",
    "Handoffs and Agents-as-tools are similar:\n",
    "\n",
    "In both cases, an Agent can collaborate with another Agent\n",
    "\n",
    "With tools, control passes back\n",
    "\n",
    "With handoffs, control passes across\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "subject_instructions = \"You can write a subject for a cold sales email. \\\n",
    "You are given a message and you need to write a subject for an email that is likely to get a response.\"\n",
    "\n",
    "html_instructions = \"You can convert a text email body to an HTML email body. \\\n",
    "You are given a text email body which might have some markdown \\\n",
    "and you need to convert it to an HTML email body with simple, clear, compelling layout and design.\"\n",
    "\n",
    "subject_writer = Agent(name=\"Email subject writer\", instructions=subject_instructions, model=\"gpt-4o-mini\")\n",
    "subject_tool = subject_writer.as_tool(tool_name=\"subject_writer\", tool_description=\"Write a subject for a cold sales email\")\n",
    "\n",
    "html_converter = Agent(name=\"HTML email body converter\", instructions=html_instructions, model=\"gpt-4o-mini\")\n",
    "html_tool = html_converter.as_tool(tool_name=\"html_converter\",tool_description=\"Convert a text email body to an HTML email body\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@function_tool\n",
    "def send_simple_notification(message: str) -> Dict[str, str]:\n",
    "    \"\"\" Send a simple notification with the given message to all sales prospects \"\"\"\n",
    "    url = \"https://api.pushover.net/1/messages.json\"\n",
    "    \n",
    "    data = {\n",
    "        \"token\": os.environ.get('PUSHOVER_TOKEN'),\n",
    "        \"user\": os.environ.get('PUSHOVER_USER'),\n",
    "        \"message\": message,\n",
    "        \"title\": \"Sales Email\"\n",
    "    }\n",
    "    \n",
    "    response = requests.post(url, data=data)\n",
    "    if response.status_code == 200:\n",
    "        return {\"status\": \"success\", \"message\": \"Notification sent successfully\"}\n",
    "    else:\n",
    "        return {\"status\": \"error\", \"message\": f\"Error {response.status_code}: {response.text}\"}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tools = [send_simple_notification]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tools"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "instructions = \"You are a notification sender. You receive the body of an email to be sent. \\\n",
    "You use the send_simple_notification tool to send the email content as a clean, readable notification. \\\n",
    "Do not add any formatting or subjects - just send the email content directly.\"\n",
    "\n",
    "\n",
    "notification_agent = Agent(\n",
    "    name=\"Notification Sender\",\n",
    "    instructions=instructions,\n",
    "    tools=tools,\n",
    "    model=\"gpt-4o-mini\",\n",
    "    handoff_description=\"Send the email content as a simple notification\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Now we have 3 tools and 1 handoff"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Only the sales agent tools for the manager - NO send_notification tool\n",
    "manager_tools = [tool1, tool2, tool3]\n",
    "handoffs = [notification_agent]\n",
    "print(\"Manager tools:\", manager_tools)\n",
    "print(\"Handoffs:\", handoffs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Improved instructions thanks to Jessica Kuijer\n",
    "\n",
    "sales_manager_instructions = \"\"\"\n",
    "You are a Sales Manager at ComplAI. Your goal is to find the single best cold sales email using the sales_agent tools.\n",
    " \n",
    "Follow these steps carefully:\n",
    "1. Generate Drafts: Use all three sales_agent tools to generate three different email drafts. Do not proceed until all three drafts are ready.\n",
    " \n",
    "2. Evaluate and Select: Review the drafts and choose the single best email using your judgment of which one is most effective.\n",
    "You can use the tools multiple times if you're not satisfied with the results from the first try.\n",
    " \n",
    "3. Handoff for Sending: DO NOT use any send_notification tools yourself. Instead, hand off to the 'Notification Sender' agent with ONLY the winning email draft text. The Notification Sender will handle the actual sending.\n",
    " \n",
    "Crucial Rules:\n",
    "- You must use the sales agent tools to generate the drafts — do not write them yourself.\n",
    "- You must NEVER use send_notification tools - only hand off to the Notification Sender agent.\n",
    "- You must hand off exactly ONE email to the Notification Sender — never more than one.\n",
    "- When handing off, pass only the email text content, not multiple emails.\n",
    "\"\"\"\n",
    "\n",
    "\n",
    "sales_manager = Agent(\n",
    "    name=\"Sales Manager\",\n",
    "    instructions=sales_manager_instructions,\n",
    "    tools=manager_tools,  # Only sales agent tools, NO send_notification\n",
    "    handoffs=handoffs,\n",
    "    model=\"gpt-4o-mini\")\n",
    "\n",
    "message = \"Send out a cold sales email addressed to Dear CEO from Alice\"\n",
    "\n",
    "with trace(\"Automated SDR\"):\n",
    "    result = await Runner.run(sales_manager, message)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Remember to check the trace\n",
    "\n",
    "https://platform.openai.com/traces\n",
    "\n",
    "And then check your Pushover notifications!!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/exercise.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#ff7800;\">Exercise</h2>\n",
    "            <span style=\"color:#ff7800;\">Can you identify the Agentic design patterns that were used here?<br/>\n",
    "            What is the 1 line that changed this from being an Agentic \"workflow\" to \"agent\" under Anthropic's definition?<br/>\n",
    "            Try adding in more tools and Agents! You could have tools that handle the mail merge to send to a list.<br/><br/>\n",
    "            HARD CHALLENGE: research how you can have SendGrid call a Callback webhook when a user replies to an email,\n",
    "            Then have the SDR respond to keep the conversation going! This may require some \"vibe coding\" 😂\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/business.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#00bfff;\">Commercial implications</h2>\n",
    "            <span style=\"color:#00bfff;\">This is immediately applicable to Sales Automation; but more generally this could be applied to  end-to-end automation of any business process through conversations and tools. Think of ways you could apply an Agent solution\n",
    "            like this in your day job.\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Extra note:\n",
    "\n",
    "Google has released their Agent Development Kit (ADK). It's not yet got the traction of the other frameworks on this course, but it's getting some attention. It's interesting to note that it looks quite similar to OpenAI Agents SDK. To give you a preview, here's a peak at sample code from ADK:\n",
    "\n",
    "```\n",
    "root_agent = Agent(\n",
    "    name=\"weather_time_agent\",\n",
    "    model=\"gemini-2.0-flash\",\n",
    "    description=\"Agent to answer questions about the time and weather in a city.\",\n",
    "    instruction=\"You are a helpful agent who can answer user questions about the time and weather in a city.\",\n",
    "    tools=[get_weather, get_current_time]\n",
    ")\n",
    "```\n",
    "\n",
    "Well, that looks familiar!\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
